| 1 | import type { APIContext, GetStaticPathsResult } from "astro"; |
| 2 | import { getCollection, getEntryBySlug } from "astro:content"; |
| 3 | import satori, { SatoriOptions } from "satori"; |
| 4 | import { html } from "satori-html"; |
| 5 | import { Resvg } from "@resvg/resvg-js"; |
| 6 | import siteConfig from "@/site-config"; |
| 7 | import { getFormattedDate } from "@/utils"; |
| 8 | |
| 9 | const monoFontReg = await fetch( |
| 10 | "https://api.fontsource.org/v1/fonts/roboto-mono/latin-400-normal.ttf" |
| 11 | ); |
| 12 | |
| 13 | const monoFontBold = await fetch( |
| 14 | "https://api.fontsource.org/v1/fonts/roboto-mono/latin-700-normal.ttf" |
| 15 | ); |
| 16 | |
| 17 | const ogOptions: SatoriOptions = { |
| 18 | width: 1200, |
| 19 | height: 630, |
| 20 | // debug: true, |
| 21 | embedFont: true, |
| 22 | fonts: [ |
| 23 | { |
| 24 | name: "Roboto Mono", |
| 25 | data: await monoFontReg.arrayBuffer(), |
| 26 | weight: 400, |
| 27 | style: "normal", |
| 28 | }, |
| 29 | { |
| 30 | name: "Roboto Mono", |
| 31 | data: await monoFontBold.arrayBuffer(), |
| 32 | weight: 700, |
| 33 | style: "normal", |
| 34 | }, |
| 35 | ], |
| 36 | }; |
| 37 | |
| 38 | const markup = (title: string, pubDate: string, description: string) => html`<div |
| 39 | tw="flex flex-col w-full h-full bg-[#000000] text-[#FFFFFF]" |
| 40 | > |
| 41 | <div tw="flex flex-col flex-1 w-full p-10 justify-center"> |
| 42 | <p tw="text-2xl mb-6">${pubDate}</p> |
| 43 | <h1 tw="text-6xl font-bold leading-snug text-white">${title}</h1> |
| 44 | <h2 tw="text-2xl font-bold leading-snug text-white">${description}</h2> |
| 45 | </div> |
| 46 | <div tw="flex items-center justify-between w-full p-10 border-t border-[#74c7ec] text-xl"> |
| 47 | <div tw="flex items-center"> |
| 48 | <svg height="60" fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> |
| 49 | <path |
| 50 | d="M20.1465 448.094H479L350.926 226.37L311.281 258.273L275.108 232.751L249.573 258.273L215.528 232.751L193.185 258.273L151.291 221.053L20.1465 448.094Z" |
| 51 | fill="#74c7ec" |
| 52 | /> |
| 53 | <path |
| 54 | d="M249.573 50.9053L151.291 221.053L193.185 258.273L215.528 232.751L249.573 258.273L275.108 232.751L311.281 258.273L350.926 226.37L249.573 50.9053Z" |
| 55 | fill="#FFFFFF" |
| 56 | /> |
| 57 | <path |
| 58 | d="M151.291 221.053L20.1465 448.094H479L350.926 226.37M151.291 221.053L249.573 50.9053L350.926 226.37M151.291 221.053L193.185 258.273L215.528 232.751L249.573 258.273L275.108 232.751L311.281 258.273L350.926 226.37" |
| 59 | stroke="black" |
| 60 | stroke-width="7" |
| 61 | /> |
| 62 | <line x1="265.341" y1="167.541" x2="294.587" y2="218.169" stroke="black" stroke-width="5" /> |
| 63 | </svg> |
| 64 | <p tw="ml-3 font-semibold text-3xl">${siteConfig.title}</p> |
| 65 | </div> |
| 66 | </div> |
| 67 | </div>`; |
| 68 | |
| 69 | export async function get({ params: { slug } }: APIContext) { |
| 70 | const post = await getEntryBySlug("post", slug!); |
| 71 | const title = post?.data.title ?? siteConfig.title; |
| 72 | const postDate = getFormattedDate(post?.data.publishDate ?? Date.now(), { |
| 73 | weekday: "long", |
| 74 | }); |
| 75 | const description = post?.data.description ?? siteConfig.title; |
| 76 | const svg = await satori(markup(title, postDate, description), ogOptions); |
| 77 | const png = new Resvg(svg).render().asPng(); |
| 78 | return { |
| 79 | body: png, |
| 80 | encoding: "binary", |
| 81 | }; |
| 82 | } |
| 83 | |
| 84 | export async function getStaticPaths(): Promise<GetStaticPathsResult> { |
| 85 | const posts = await getCollection("post"); |
| 86 | return posts.filter(({ data }) => !data.ogImage).map(({ slug }) => ({ params: { slug } })); |
| 87 | } |